Tooling to rebuild and flash signed FIP (Firmware Image Package) bundles for the Spotify Car Thing (Amlogic G12A "superbird"), letting you boot your own u-boot / BL31 / kernel chain at power-on instead of the vendor firmware.
Built and verified during a long night of reverse engineering 2026-05-13. The hardware is fully owned. There is no Spotify branding, splash, or firmware running once this is flashed.
The primary toolkit is now
fip-tool/— a single pure-Go binary (ramboot/decrypt/flash/sign). Itssigndoes the whole job in Go — stage-1 FIP assembly and signing — with noaml_encrypt_g12a, noamlogic-boot-fipclone, and no shell; the resulting FIP body is byte-identical to the hardware-validated image. The original Python/shell scripts that pioneered all this live inpython/and still work, but Go is the maintained path.
fip-tool/— the Go toolkit: RAM-boot a FIP via mask-ROM (ramboot),decryptSpotify-signed FIP/DTB blobs, build +flasha boot0/boot1 image, andsigna u-boot into a bootable FIP (pure-Go by default;--vendorfalls back toaml_encrypt_g12a).python/— the legacy scripts:fip-rebuild.sh(wrapsaml_encrypt_g12a),flash_boot_partition.py, andaml_decrypt.py(decrypts FIP/DTB blobs for analysis). Superseded byfip-toolbut kept for reference / cross-checking.- Docs explaining the secure-boot status, BL2's fallback chain, eMMC layout, and recovery procedures.
Spotify open-sourced their u-boot fork at https://github.com/spsgsb/uboot
and committed board/amlogic/superbird_production/aml-user-key.sig —
the production AES + RSA signing keys for the device. We use those keys
to sign+encrypt any FIP content (reproduced entirely in pure Go by
fip-tool sign; originally via the vendor aml_encrypt_g12a --bootsig), and
BL2 (whose verification key was baked in at signing time) accepts it as if
Spotify produced it themselves.
We can't replace BL2 itself — mask ROM RSA-verifies it against a fused public-key hash. But we don't need to. BL2 is happy initializing DDR and handing off to whatever FIP it accepts as signed.
Empirical proof the leaked key is genuine: re-encrypting the FIP HDR
with this key produces byte-identical 48-byte ciphertext to stock
bootloader.dump's FIP HDR. Same key, same IV → same ciphertext on
same plaintext. P(coincidence) = 2⁻³⁸⁴.
See docs/secure-boot.md for the full story.
# 1. One-time setup (clones amlogic-boot-fip, checks deps, verifies key)
./setup.sh
# 2. Build the Go toolkit
cd fip-tool && go build -o fip-tool . && cd ..
# 3. Build your u-boot (or use any mainline u-boot binary)
cd ../superbird-uboot # for example
make spotify_carthing_defconfig
make -j$(nproc) CROSS_COMPILE=aarch64-linux-gnu-
cd ../superbird-fip-tools
# 4. Wrap u-boot.bin in a signed FIP — pure Go, no vendor binary / clone / shell
fip-tool/fip-tool sign ../superbird-uboot/u-boot.bin
# (swap in your own TF-A with --bl31 your/bl31.bin)
# 5. Get device into mask-ROM USB mode (buttons 1+4 + reset), then flash:
fip-tool/fip-tool flash ours # stock BL2 + your signed FIP → boot0/boot1
# 6. Release buttons. Power-cycle. Your u-boot runs at power-on.The Go
fip-tool signneeds nothing but the repo itself — noamlogic-boot-fipclone, noaml_encrypt_g12a, nosetup.sh.setup.shis only needed for the legacypython/scripts orsign --vendor.
For digging into vendor binaries — comparing firmware versions, hunting
panel-init values, reverse-engineering, etc. — aml_decrypt.py unpacks
the same FIP envelope this repo builds.
# one-time: install the only Python dependency
pip install -r python/requirements.txt # or: pip install pycryptodome
# decrypt a 4 MiB bootloader.dump (BL2 + FIP body); print sub-section map
python/aml_decrypt.py bootloader.dump -o /tmp/fip.bin --map-sections
# decrypt a standalone fip_a / fip_b partition
python/aml_decrypt.py --fip fip_a.dump -o /tmp/fip.bin
# decrypt a DTB partition + locate FDT blobs inside
python/aml_decrypt.py --dtb dtb_partition.bin -o /tmp/dtbs.bin --scan-fdts
# raw AES-256-CBC zero-IV decrypt of any blob signed with this key
python/aml_decrypt.py --raw blob.enc -o blob.bin
# print just the AES key (extracted from the bundle)
python/aml_decrypt.py --show-key dummyWhat this CAN'T do: BL33 inside the FIP is LZ4-compressed before
AES, and BL31 decompresses it at runtime — there's no public spec for
the Amlogic LZ4 wrapper format. If you want the plaintext BL33 binary,
RAM-dump it from a live device at the relocation address (typically
~0x17E42000 on G12A) — see docs/dump-vendor-bl33.md. The decryptor
will get you everything else (FIP HDR, BL30, BL31, signing tables).
Provenance: this just uses the public key Spotify open-sourced via
spsgsb/uboot:board/amlogic/superbird_production/aml-user-key.sig. The
key extraction logic, AES mode (CBC), and IV (zero) were verified by
brute-forcing every 16/32-byte window in the keybundle against a known
plaintext-ciphertext pair from real device data. See the docstring at
the top of aml_decrypt.py for full forensic trail.
- A working build of u-boot (or another BL33 of your choice).
- A stock bootloader image — shipped here as
stock.bootloader.bin(full 4 MiB: info region + BL2 + vendor FIP). It's common stock firmware, identical across units, so you don't need to dump your own. It's the default--stock-bootloaderfor both theoursandstockbuilds, and the inputaml_decrypt.pyexpects. (If you'd rather use your own unit's image:superbird-tool --dump_partition bootloader bootloader.dump, then pass it via--stock-bootloader.) superbird-toolfor getting the device into USB Burn Mode and for general device manipulation. The flash script here usespyamlbootdirectly so it doesn't depend on superbird-tool's library, but you still need superbird-tool to enter burn mode.- pyamlboot (
sudo pip install git+https://github.com/superna9999/pyamlboot).
.
├── README.md
├── LICENSE
├── setup.sh one-shot dependency setup
├── fip-tool/ Go toolkit: ramboot / decrypt / flash / sign — THE MAIN TOOL
├── python/ legacy Python/shell tools (superseded by fip-tool)
│ ├── fip-rebuild.sh wraps aml_encrypt_g12a → signed FIP
│ ├── flash_boot_partition.py builds info_sector + hybrid + flashes
│ ├── aml_decrypt.py decrypt FIP bodies / DTB partitions for analysis
│ └── requirements.txt pycryptodome (the only Python dep)
├── stock.bootloader.bin stock boot partition (info + BL2 + vendor FIP), 4 MiB
├── keys/
│ ├── aml-user-key.sig spotify production key (from spsgsb/uboot)
│ └── NOTICE.md provenance, ethics, proof
├── docs/
│ ├── boot-chain.md mask ROM → BL2 fallback chain explainer
│ ├── emmc-layout.md physical layout, info_sector struct
│ ├── flash-howto.md detailed operator walkthrough
│ └── secure-boot.md high-level "what's mutable" reference
└── (after ./setup.sh)
└── amlogic-boot-fip/ cloned LibreELEC repo with build-fip.sh
fip-tool/ is a cross-platform, pure-Go reimplementation of the
device-side tooling — one binary, four subcommands (the only C is cgo→libusb
for raw USB; no external binaries/scripts at all):
| command | replaces |
|---|---|
fip-tool ramboot <fip> (RAM-load via mask-ROM) |
superbird-tool --burn_mode |
fip-tool decrypt |
aml_decrypt.py |
fip-tool flash |
flash_boot_partition.py |
fip-tool sign |
fip-rebuild.sh |
All four are pure Go — libusb + stdlib crypto, no pyamlboot/pycryptodome,
no aml_encrypt_g12a, no amlogic-boot-fip clone, no shell. The AES + RSA
production keys are extracted from aml-user-key.sig in pure Go. ramboot is
hardware-verified; decrypt/flash are byte-identical to the Python tools; sign
assembles the stage-1 FIP natively (embedded immutable prefix + your BL31/BL33)
and re-signs it natively, producing a FIP whose bootable body is byte-identical
to the hardware-validated image. The Python tools are fully superseded.
cd fip-tool && go build -o fip-tool .
# device in mask-ROM USB mode (hold buttons 1+4, reset), then:
./fip-tool ramboot ../out/u-boot.bin.spotify.encryptSee fip-tool/README.md for all subcommands, build
deps (libusb), and the native-sign analysis.
The mask ROM USB Mode path is always available, regardless of eMMC state. Even with the entire user area zeroed and corrupted boot0/boot1 hardware partitions:
- Hold buttons 1+4 and reset
- Device enumerates as
1b8e:c003 GX-CHIP superbird-tool --burn_modeuploads vendor BL2 + bootloader.img via USB and lands you in vendor burn-mode u-boot- Restore boot0/boot1 with
python/flash_boot_partition.py stockor rebuild from scratch
Empirically verified by wiping the eMMC user area down to zeros and re-flashing everything from USB.
| Stage | Status |
|---|---|
| FIP body replacement (BL33) | ✅ Verified working |
| Boot at power-on from re-signed FIP | ✅ Verified working |
| Full user-area wipe + recovery | ✅ Verified working |
| FIP body decryption (stock analysis) | ✅ Verified working |
| Replacing BL2 | ❌ Not possible (fuse-locked) |
| Replacing BL31 with TF-A | ✅ Verified working (mainline 2.14) |
| Mainline kernel chainload | ⏳ Future work |
| GPT user-area partitioning | ⏳ Future work |
The Car Thing was discontinued by Spotify in 2024 with an announcement that they'd brick devices server-side. Public outcry pulled them back, but the device remains an orphan. With this tooling, owners can run their own software stack on hardware they bought outright.
The leaked key was already public — this just makes using it ergonomic.
- superbird-tool —
general device-manipulation tool,
--burn_mode, partition dump/restore, chainload via USB. - LibreELEC/amlogic-boot-fip —
upstream of
aml_encrypt_g12aand per-board BL30/BL31 components. - spsgsb/uboot — Spotify's vendor u-boot fork. Where the key came from.
MIT. See LICENSE. keys/aml-user-key.sig is reproduced from
spsgsb/uboot and is not authored by this project — see
keys/NOTICE.md.